import { CommandType, PathCommand, Point, PointTuple } from './types'

/**
 * 旋转点
 *
 * @param point 要旋转的点
 * @param angle 旋转角度
 */
export function rotatePoint(point: Point, angle: number) {
    const nx = point.x * Math.cos(angle) - point.y * Math.sin(angle)
    const ny = point.y * Math.cos(angle) + point.x * Math.sin(angle)
    point.x = nx
    point.y = ny
}

/**
 * 平移点
 * @param point 要平移的点
 * @param dx 水平方向平移距离
 * @param dy 垂直方向平移距离
 */
export function translatePoint(point: Point, dx: number, dy: number) {
    point.x += dx
    point.y += dy
}

/**
 * 缩放点
 * @param point 要缩放的点
 * @param s 缩放比例
 */
export function scalePoint(point: Point, s: number) {
    point.x *= s
    point.y *= s
}

/**
 * 构造圆弧路径点
 *
 * @param x 圆心 x 坐标
 * @param y 圆心 y 坐标
 * @param radius 半径
 * @param startAngle 起始角度
 * @param endAngle 结束角度
 * @param anticlockwise 是否逆时针
 * @param stepAngle 每隔多少弧度取一个点，默认为 15 度
 *
 * @returns 圆弧路径点数组
 */
export function buildArcPoint(
    x: number,
    y: number,
    radius: number,
    startAngle: number,
    endAngle: number,
    anticlockwise: boolean,
    stepAngle = (15 * Math.PI) / 180,
): PointTuple[] {
    const points: PointTuple[] = []
    const angle = anticlockwise ? -stepAngle : stepAngle
    let currentAngle = startAngle

    while (anticlockwise ? currentAngle > endAngle : currentAngle < endAngle) {
        points.push([
            Number((x + Math.cos(currentAngle) * radius).toFixed(2)),
            Number((y + Math.sin(currentAngle) * radius).toFixed(2)),
        ])
        currentAngle += angle
    }

    return points
}

/**
 * 构建二次贝塞尔曲线
 *
 * @param x1 起始点 x 坐标
 * @param y1 起始点 y 坐标
 * @param x2 控制点 x 坐标
 * @param y2 控制点 y 坐标
 * @param x3 结束点 x 坐标
 * @param y3 结束点 y 坐标
 * @param step 每隔多少取一个点，默认为 0.1
 *
 * @returns 二次贝塞尔曲线路径点数组
 */
export function buildBezierCurve(
    x1: number,
    y1: number,
    x2: number,
    y2: number,
    x3: number,
    y3: number,
    step = 0.1,
): PointTuple[] {
    const points: PointTuple[] = []
    let t = 0
    let mt = 1 - t
    while (t <= 1) {
        const x = mt ** 2 * x1 + 2 * t * mt * x2 + t * t * x3
        const y = mt ** 2 * y1 + 2 * t * mt * y2 + t * t * y3
        points.push([x, y])
        t += step
        mt = 1 - t
    }

    return points
}

/**
 * 构建三次贝塞尔曲线
 *
 * @param x1 起始点 x 坐标
 * @param y1 起始点 y 坐标
 * @param x2 控制点1 x 坐标
 * @param y2 控制点1 y 坐标
 * @param x3 控制点2 x 坐标
 * @param y3 控制点2 y 坐标
 * @param x4 结束点 x 坐标
 * @param y4 结束点 y 坐标
 * @param step 每隔多少取一个点，默认为 0.1
 *
 * @returns 三次贝塞尔曲线路径点数组
 */
export function buildCubicCurve(
    x1: number,
    y1: number,
    x2: number,
    y2: number,
    x3: number,
    y3: number,
    x4: number,
    y4: number,
    step = 0.1,
): PointTuple[] {
    const points: PointTuple[] = []
    let t = 0
    let mt = 1 - t
    while (t <= 1) {
        const x = mt ** 3 * x1 + 3 * t * mt ** 2 * x2 + 3 * t ** 2 * mt * x3 + t ** 3 * x4
        const y = mt ** 3 * y1 + 3 * t * mt ** 2 * y2 + 3 * t ** 2 * mt * y3 + t ** 3 * y4
        points.push([x, y])
        t += step
        mt = 1 - t
    }

    return points
}

/**
 * 构建路径点数组
 *
 * @param commands 路径命令数组
 *
 * @returns 路径点数组
 */
export function buildPath(commands: PathCommand[]): PointTuple[] {
    const points: PointTuple[] = []
    let arcPoints: PointTuple[] = []

    let x = 0
    let y = 0
    let endAngle: number
    let startAngle: number
    let largeArcFlag: boolean
    let sweepFlag: boolean
    let endPoint: Point
    let midPoint: Point
    let angle: number
    let lambda: number
    let t1: number
    let t2: number
    let rx: number
    let ry: number
    let ctype: CommandType
    let centerPoint: Point
    let cpx: null | number = null
    let cpy: null | number = null
    let qcpx: null | number = null
    let qcpy: null | number = null
    let startPoint: null | Point = null
    let currentPoint: Point = { x: 0, y: 0 }

    commands.forEach((c) => {
        ctype = c[0]

        // 重置控制点
        if (ctype !== 'S' && ctype !== 's' && ctype !== 'C' && ctype !== 'c') {
            cpx = null
            cpy = null
        }

        if (ctype !== 'T' && ctype !== 't' && ctype !== 'Q' && ctype !== 'q') {
            qcpx = null
            qcpy = null
        }

        switch (c[0]) {
            case 'M':
            case 'm':
                if (ctype === 'm') {
                    x += c[1]
                    y += c[2]
                } else {
                    x = c[1]
                    y = c[2]
                }

                if (ctype === 'M' || !startPoint) {
                    startPoint = { x, y }
                }

                points.push([x, y])
                break

            case 'l':
                x += c[1]
                y += c[2]
                points.push([x, y])
                break

            case 'L':
                x = c[1]
                y = c[2]
                points.push([x, y])
                break

            case 'H':
                x = c[1]
                points.push([x, y])
                break

            case 'h':
                x += c[1]
                points.push([x, y])
                break

            case 'V':
                y = c[1]
                points.push([x, y])
                break

            case 'v':
                y += c[1]
                points.push([x, y])
                break

            case 'a':
            case 'A':
                if (ctype === 'a') {
                    x += c[6]
                    y += c[7]
                } else {
                    x = c[6]
                    y = c[7]
                }

                rx = c[1]
                ry = c[2]
                angle = (c[3] * Math.PI) / 180
                largeArcFlag = !!c[4]
                sweepFlag = !!c[5]
                endPoint = { x, y }

                // https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes

                midPoint = {
                    x: (currentPoint.x - endPoint.x) / 2,
                    y: (currentPoint.y - endPoint.y) / 2,
                }
                rotatePoint(midPoint, -angle)

                // radius correction
                lambda = (midPoint.x * midPoint.x) / (rx * rx) + (midPoint.y * midPoint.y) / (ry * ry)
                if (lambda > 1) {
                    lambda = Math.sqrt(lambda)
                    rx *= lambda
                    ry *= lambda
                }

                centerPoint = {
                    x: (rx * midPoint.y) / ry,
                    y: -(ry * midPoint.x) / rx,
                }
                t1 = rx * rx * ry * ry
                t2 = rx * rx * midPoint.y * midPoint.y + ry * ry * midPoint.x * midPoint.x
                if (sweepFlag !== largeArcFlag) {
                    scalePoint(centerPoint, Math.sqrt((t1 - t2) / t2) || 0)
                } else {
                    scalePoint(centerPoint, -Math.sqrt((t1 - t2) / t2) || 0)
                }

                startAngle = Math.atan2((midPoint.y - centerPoint.y) / ry, (midPoint.x - centerPoint.x) / rx)
                endAngle = Math.atan2(-(midPoint.y + centerPoint.y) / ry, -(midPoint.x + centerPoint.x) / rx)

                rotatePoint(centerPoint, angle)
                translatePoint(centerPoint, (endPoint.x + currentPoint.x) / 2, (endPoint.y + currentPoint.y) / 2)

                arcPoints = buildArcPoint(centerPoint.x, centerPoint.y, 1, startAngle, endAngle, !sweepFlag)
                points.push(
                    ...arcPoints.map(([px, py]) => {
                        const p = { x: px, y: py }
                        rotatePoint(p, angle)
                        p.x *= rx
                        p.y *= ry
                        return [p.x, p.y] as PointTuple
                    }),
                    [endPoint.x, endPoint.y],
                )

                break

            case 'C':
                cpx = c[3]
                cpy = c[4]
                x = c[5]
                y = c[6]
                arcPoints = buildCubicCurve(currentPoint.x, currentPoint.y, c[1], c[2], cpx, cpy, x, y)
                points.push(...arcPoints, [x, y])
                break

            case 'c':
                arcPoints = buildCubicCurve(
                    currentPoint.x,
                    currentPoint.y,
                    c[1] + x,
                    c[2] + y,
                    c[3] + x,
                    c[4] + y,
                    c[5] + x,
                    c[6] + y,
                )
                cpx = c[3] + x
                cpy = c[4] + y
                x += c[5]
                y += c[6]
                points.push(...arcPoints, [x, y])
                break

            case 'S':
                if (cpx === null || cpy === null) {
                    cpx = x
                    cpy = y
                }

                arcPoints = buildCubicCurve(
                    currentPoint.x,
                    currentPoint.y,
                    2 * x - cpx,
                    2 * y - cpy,
                    c[1],
                    c[2],
                    c[3],
                    c[4],
                )
                cpx = c[1]
                cpy = c[2]
                x = c[3]
                y = c[4]
                points.push(...arcPoints, [x, y])
                break

            case 's':
                if (cpx === null || cpy === null) {
                    cpx = x
                    cpy = y
                }

                arcPoints = buildCubicCurve(
                    currentPoint.x,
                    currentPoint.y,
                    2 * x - cpx,
                    2 * y - cpy,
                    c[1] + x,
                    c[2] + y,
                    c[3] + x,
                    c[4] + y,
                )

                cpx = c[1] + x
                cpy = c[2] + y
                x += c[3]
                y += c[4]
                points.push(...arcPoints, [x, y])
                break

            case 'Q':
                qcpx = c[1]
                qcpy = c[2]
                x = c[3]
                y = c[4]
                arcPoints = buildBezierCurve(currentPoint.x, currentPoint.y, qcpx, qcpy, x, y)
                points.push(...arcPoints, [x, y])
                break

            case 'q':
                qcpx = c[1] + x // last control point
                qcpy = c[2] + y
                x += c[3]
                y += c[4]
                arcPoints = buildBezierCurve(currentPoint.x, currentPoint.y, qcpx, qcpy, x, y)
                points.push(...arcPoints, [x, y])
                break

            case 'T':
                if (qcpx === null || qcpy === null) {
                    qcpx = x
                    qcpy = y
                }
                qcpx = 2 * x - qcpx // last control point
                qcpy = 2 * y - qcpy
                x = c[1]
                y = c[2]
                arcPoints = buildBezierCurve(currentPoint.x, currentPoint.y, qcpx, qcpy, x, y)
                points.push(...arcPoints, [x, y])
                break

            case 't':
                if (qcpx === null || qcpy === null) {
                    qcpx = x
                    qcpy = y
                }
                qcpx = 2 * x - qcpx
                qcpy = 2 * y - qcpy
                x += c[1]
                y += c[2]
                arcPoints = buildBezierCurve(currentPoint.x, currentPoint.y, qcpx, qcpy, x, y)
                points.push(...arcPoints, [x, y])
                break

            case 'z':
            case 'Z':
                if (startPoint) {
                    x = startPoint.x
                    y = startPoint.y
                }
                startPoint = null
                points.push([x, y])
                break
        }

        if (!currentPoint) {
            currentPoint = { x, y }
        } else {
            currentPoint.x = x
            currentPoint.y = y
        }
    })

    return points
}
